<?php
/*
 * Copyright (c) 2008 Sergey Bondari, bondari@bondari.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

namespace Mosaic\NS;


/**
 * Nameserver
 * 
 * Uses network event machine-like multiplexed communication. Internally, naming agent uses mapping of streams to to integers.
 *
 * @package System
 * @subpackage Nameserver
 */
class Nameserver extends \Mosaic\System\GenericRequestBroker {
	/**
 	 * Simulate name of the node for some library functions
	 * @var string
	 */
	public $name = 'nameserver';
	
	/**
 	 * Configuration instance
	 * @var Config
	 */
	public $config;

	/**
 	 * Debugging handlers
	 * @var Debug
	 */
	public $debug;

	/**
	 * Holds open control sockets to nodes 
	 * @var array
	 */
	private $nodeSockets ;


	/**
	 * Holds set of all active sockets
	 * @var array
	 */
	private $activeSockets;

	/**
	 * Holds mappings of remote classes to nodes
	 * @var array
	 */
	private $remoteClasses;

	/**
	 * Holds NS metadata for nodes
	 * @var array
	 */
	private $nodes;


	/**
	 * Class constructor
	 * @param Config
	 */
	function __construct(\Mosaic\System\Config $config) {
		parent::__construct();
		$this->config = $config;
		$this->nodeSockets = array();
		$this->activeSockets = array();
		$this->remoteClasses = array();
		$this->nodes = array();
	}


	/**
	 * Entry point.
	 * This function is called to start the nameserver
	 */
	public function start() {
		$this->initDebug();
		trigger_error('System started.');
		$this->run();
	}

	/**
	 * Debugging initialization
	 * @return void
	 */
	private function initDebug() {
		$this->debug = new \Mosaic\System\Debug();
	}


	/**
	 * Role getter 
	 * Always returns NAMESERVER (for compatibility with common system libraries code).
	 * @returns \Mosaic\System\NAMESERVER
	 */
	public function role() {
		return \Mosaic\System\NAMESERVER;
	}


	/**
	 * Server loop
	 * @return void
	 */
	public function run() {
		/*
		 * Open main NS socket.
		 */
		trigger_error('Opening new NS socket on '.$this->config->get('net.listen'));
		$this->extSocket = stream_socket_server($this->config->get('net.listen'), $errno, $errstr);

		if (!$this->extSocket) {
			throw new \Mosaic\System\IOException('Socket creation failed. ('.$errno.'): '.$errstr);
		} else {
			trigger_error('External socket created');
		}

		/*
		 * Register known node sockets
		 */
		$this->extSocketID = $this->registerSocket($this->extSocket);


			while (1) {
				try {
				/*
				 * Always receive requests on the external socket
				 * and internode sockets
				 */
				$socketsToRead = array_merge(array($this->extSocket), $this->activeSockets);


				/**
				 * Add all sockets from queue to write event
				 */
				$socketsToWrite = array();
				$_sids = array();
				foreach  ($this->queue as $_ => $queue) {
					if (!isset($_sids[$_]) && $queue->count()) $socketsToWrite[] = $this->socket($_);
				}
				trigger_error('Nameserver is entering select(). '.count($socketsToRead).' sockets to read. '.count($socketsToWrite).' sockets to write.');


				/*
				 * Wait for socket event
				 */
				$stat = stream_select($socketsToRead, $socketsToWrite, $except = NULL, NULL);

				/* Unexpected behaviour */
				if ($stat === false) {
					trigger_error('stream_select() did not return any sockets.',E_USER_ERROR);
					continue;
				}

				trigger_error('Incoming socket event');

				/*
				 * Process incoming requests
				 */
				foreach ($socketsToRead as $socket) {
					$socketID = $this->socketId($socket);
					/*
					 * New external request/response
					 */
					if ($socket == $this->extSocket) {
						/* Open new socket for this request */
						if (!$_socket = stream_socket_accept($socket)) {
							throw new IOException('Could not socket_accept()');
						}
						$socketID = $this->registerSocket($_socket);
						$socket = $_socket;

						$this->activeSockets[$socketID] = $_socket;
						
					}

					if (feof($socket)) {
						trigger_error('Remote party has closed connection.');
						unset($this->activeSockets[$socketID]);
						$this->unregisterSocket($socket);
						continue;
					}
					/* Parse request as MCGI */
					try {
						$mcgi = new \Mosaic\MCGI\Packet();
						$mcgi->receive($socket);
					} catch (\Mosaic\System\IOException $e) {
						/* Log error. Close socket. */
						trigger_error('Error parsing MCGI request: '.$e->getMessage(), E_USER_ERROR);
						$this->unregisterSocket($socket);
						continue;
					}

					switch ($mcgi->protocol()) {
						case \Mosaic\MCGI\SCGI:
							throw new \Mosaic\System\RuntimeException('Unexpected protocol version for this socket.');
						break;
						case \Mosaic\MCGI\MCGI;
							if ($mcgi->getHeader('RESPONSE')) {
								$response = \Mosaic\System\Response::createFromMCGIPacket($mcgi);
								$this->processResponse($response, $socketID);
							} else {
								$request = \Mosaic\System\Request::createFromMCGIPacket($mcgi);
								$this->processRequest($request, $socketID);
							}
						break;
					}
				}

				/*
				 * Process outgoing requests
				 */
				foreach ($socketsToWrite as $socket) {
					$socketID = $this->socketId($socket);
					trigger_error('Socket '.$socketID.' is ready to receive data. Extracting request.');
					$r = $this->queue[$socketID]->extract();
					trigger_error('Preparing MCGI packet');
					$mcgi = $r->createMCGIPacket($this->socketProtocol($socketID));
					trigger_error('Sending MCGI packet');
					try {
						$mcgi->send($socket);
					} catch (\Mosaic\System\IOException $e) {
						trigger_error('Socket did not accept all data. Aborting and unregistering.', E_USER_ERROR);
						$this->unregisterSocket($socketID);
					} catch (\Mosaic\System\RuntimeException $e) {
						trigger_error('Runtime exception during send. Forcing silent socket unregistration.', E_USER_ERROR);
						try {
							$this->unregisterSocket($socketID);
						} catch (\Mosaic\System\RuntimeException $e) {
							trigger_error('Registration failed. Socket is already unregistered.', E_USER_WARNING);
						}

					}
				}
				
				// process

				trigger_error('Socket event processed.');


				} catch (\Exception $e) {
					trigger_error('[Exception] '.get_class($e).': '.$e->getMessage(), E_USER_ERROR);
				}


			}


		$this->unregisterSocket($this->extSocketID);


	}


	
	/**
	 * Unregister socket
	 *
	 * @param socket Socket to unregister
	 * @return void
	 */
	protected function unregisterSocket($socket) {
		$node = array_search($socket, $this->nodeSockets);
		if ($node) {
			trigger_error($node.' has closed socket. Unregistering node.');
			unset($this->nodes[$node]);
			unset($this->nodeSockets[$node]);
			foreach ($this->remoteClasses as $className => $classData) {
				if ($classData['node'] != $node) continue;
				trigger_error('Unregistering remote object class '. $className);
				unset($this->remoteClasses[$className]);
			}


			foreach ($this->nodeSockets as $_nodeName => $_nodeSocket) {
				trigger_error('Sending "'.$node.'" node down notification to "'.$_nodeName.'"');
				$_req = new \Mosaic\System\NodeDownManifest($node);
				$this->enqueue($this->socketId($_nodeSocket), $_req, 100);
			}

		} else {
			trigger_error('Unregister of unknown socket', E_USER_WARNING);
		}


		unset($this->activeSockets[$this->socketId($socket)]);

		return parent::unregisterSocket($socket);
	}


	/**
	 * Incoming request processing routine
	 *
	 * @param Request
	 * @param integer ID of the socket that pushed the request
	 * @return void
	 */
	protected function processRequest(\Mosaic\System\Request $req, $socketID) {
		trigger_error('Processing request of type '. $req->getRequestType());
		$response = new \Mosaic\System\Response($req);
		switch ($req->getRequestType()) {
			case \Mosaic\System\RequestType::NODE_UP_MANIFEST:
				trigger_error(sprintf('Node up manifest "%s" resolves to "%s"',$req->getNodeName(),$req->getDSN()));


				if (isset($this->nodes[$req->getNodeName()])) {
					trigger_error('Node naming conflict. Unable to register a node', E_USER_ERROR);
					$response->setData(new \Mosaic\System\RuntimeException('Node name is already occupied'));
				} else {
					/*
					 * Add node-to-dsn, node-to-manifest mapping
					 */
					$this->nodes[$req->getNodeName()] = array('dsn' => $req->getDSN(), 'manifest' => $req->getManifest());

					/*
					 * Remember node socket to listen
					 */
					$this->nodeSockets[$req->getNodeName()] = $this->socket($socketID);


					/*
					 * forward node up manifest to all nodes
					 */
					foreach ($this->nodeSockets as $_nodeName => $_nodeSocket) {
						/* 
						 * Skip node that is actually going up
						 */
						if ($_nodeName == $req->getNodeName()) continue;

						trigger_error('Forwarding "'.$req->getNodeName().'.rdf" to "'.$_nodeName.'"');
						$_req = new \Mosaic\System\NodeUpManifest($req->getNodeName(),$req->getDSN(), $req->getManifest());
						$this->enqueue($this->socketId($_nodeSocket), $_req, 100);
					}

					/*
					 * forward node up manifests to this node, from all live nodes
					 */
					foreach ($this->nodes as $_nodeName => $_nodeData) {
						/* 
						 * Skip node that is actually going up
						 */
						if ($_nodeName == $req->getNodeName()) continue;

						trigger_error('Forwarding "'.$_nodeName.'.rdf" to "'.$req->getNodeName().'"');
						$_req = new \Mosaic\System\NodeUpManifest($_nodeName, $_nodeData['dsn'], $_nodeData['manifest']);
						$this->enqueue($this->socketId($this->nodeSockets[$req->getNodeName()]), $_req, 1);
					}

				}


					/*
					 * Pool request
					 */
					$this->pool($socketID, $req);
					$this->processResponse($response, $socketID);



				
			break;

			case \Mosaic\System\RequestType::NODE_DOWN_MANIFEST:
				/*
				 * Pool request
				 */
				trigger_error(sprintf('Node down manifest "%s"',$req->getNodeName()));
				$this->unregisterSocket($this->socket($socketID));
			break;

			case \Mosaic\System\RequestType::NS_REMOTE_CLASS_RESOLVE:
				/*
				 * Pool request
				 */
				$this->pool($socketID, $req);
				trigger_error(sprintf('Remote class resolve "%s"',$req->getClassName()));
				if (isset($this->nodes[$this->remoteClasses[$req->getClassName()]['node']])) {
					$response->setData($this->remoteClasses[$req->getClassName()]['node']);
				} else {
					$response->setData(new \Mosaic\System\RuntimeException('Unknown class '.$req->getClassName()));
				}
				$this->processResponse($response, $socketID);
			break;

			case \Mosaic\System\RequestType::NS_REMOTE_CLASS_MANIFEST:
				/*
				 * Pool request
				 */
				trigger_error(sprintf('Remote class manifest "%s" is provided by "%s"',$req->getClassName(),$req->getNodeName()));

				if (isset($this->remoteClasses[$req->getClassName()])) {
					trigger_error('Remote class naming conflict. Unable to register a remote class.', E_USER_ERROR);
					$response->setData(new \Mosaic\System\RuntimeException('Class name is already occupied'));
				} else {
					$this->remoteClasses[$req->getClassName()] = array('node' => $req->getNodeName(), 'stub' => $req->getStubCode());

				}
				$this->pool($socketID, $req);
				$this->processResponse($response, $socketID);
			break;

			case \Mosaic\System\RequestType::NS_NODE_RESOLVE:
				trigger_error(sprintf('Node resolve "%s"',$req->getNodeName()));
				/*
				 * Pool request
				 */
				$this->pool($socketID, $req);

				if (isset($this->nodes[$req->getNodeName()])) {
					$response->setData($this->nodes[$req->getNodeName()]['dsn']);
				} else {
					$response->setData(new \Mosaic\System\RuntimeException('Unknown node '.$req->getNodeName()));
				}

				$this->processResponse($response, $socketID);
			break;


			case \Mosaic\System\RequestType::REMOTE_OBJECT_STUB:
				trigger_error(sprintf('Stub "%s"',$req->getClassName()));
				/*
				 * Pool request
				 */
				$this->pool($socketID, $req);

				if (isset($this->remoteClasses[$req->getClassName()])) {
					$response->setData($this->remoteClasses[$req->getClassName()]['stub']);
				} else {
					$response->setData(new \Mosaic\System\RuntimeException('Unknown class '.$req->getClassName()));
				}

				$this->processResponse($response, $socketID);
			break;

			default:
				throw new Exception('Invalid request type');

		}

		return $response;
	}
	
}

